/**
 * laytpl
 * 轻量级通用模板引擎
 */

// 实例接口
var thisModule = function () {
  var that = this;
  var options = that.config;

  return {
    config: options,

    /**
     * 渲染模板
     * @param {Object} data - 模板数据
     * @param {Function} callback - 回调函数
     * @returns {string} 渲染后的模板
     */
    render: function (data, callback) {
      options.data = data;
      var html = that.render();

      // 如果传入目标元素选择器，则直接将模板渲染到目标元素中
      if (options.target) {
        var elem = document.querySelector(options.target);
        if (elem) {
          elem.innerHTML = html;
        }
      }

      // 返回结果
      return typeof callback === 'function' ? (callback(html), this) : html;
    },

    /**
     * 编译新的模板
     * @param {string} template - 模板
     * @returns {this}
     */
    compile: function (template) {
      options.template = template;
      delete that.compilerCache; // 清除模板缓存
      // that.compile(template);
      return this;
    },

    /**
     * 模板编译错误事件
     * @param {Function} callback
     * @returns {this}
     */
    error: function (callback) {
      callback && (options.error = callback);
      return this;
    },

    /**
     * 以下为兼容旧版本相关方法
     */

    // 解析并渲染模板
    parse: function (template, data) {
      return this.compile(template).render(data);
    },
  };
};

// 模板内部变量
var vars = {
  // 字符转义
  escape: function (html) {
    var exp = /[<"'>]|&(?=#?[a-zA-Z0-9]+)/g;
    if (html === undefined || html === null) return '';
    html += '';
    if (!exp.test(html)) return html;
    return html.replace(exp, function (str) {
      return '&#' + str.charCodeAt(0) + ';';
    });
  },
};

// 组件工具类方法
var tools = {
  /**
   * 创建动态正则表达式
   * @param {string} str - 表达式字符
   * @param {string} mod - 修饰符
   * @returns {RegExp} - 正则表达式
   */
  regex: function (str, mod) {
    return new RegExp(str, mod || 'g');
  },

  /**
   * 错误提示
   * @param {string} e - 原始错误信息
   * @param {Object} opts - 自定义选项
   * @param {Function} error - 错误回调
   * @returns {string} - 错误提示
   */
  error: function (e, opts, error) {
    opts = opts || {};
    opts = Object.assign(
      {
        errorContext: '',
      },
      opts,
    );

    // 向控制台输出错误信息
    var message = 'Laytpl ' + (opts.type || '') + 'Error: ' + e;
    var errorContext = opts.errorContext;

    delete opts.errorContext;
    typeof console === 'object' &&
      console.error(message, '\n', errorContext, '\n', opts);
    typeof error === 'function' && error(opts); // 执行错误回调
    return message; // 向视图返回错误提示
  },
};

// 默认配置
var config = {
  open: '{{', // 起始界定符
  close: '}}', // 结束界定符
  cache: true, // 是否开启模板缓存，以便下次渲染时不重新编译模板
  condense: true, // 是否压缩模板空白符，如：将多个连续的空白符压缩为单个空格
  tagStyle: '', // 标签风格。默认采用 < 2.11 的风格，设置 modern 则采用 2.11+ 风格
};

// 构造器
var Class = function (template, options) {
  var that = this;

  // 选项合并
  options = that.config = Object.assign(
    {
      template: template,
    },
    config,
    options,
  );

  // 当前实例的模板内工具
  that.vars = Object.assign(
    {
      /**
       * 引用外部模板。若在 Node.js 环境，可通过重置该方法实现模板文件导入
       * @param {string} id - 模板 ID
       * @param {Object} data - 模板数据
       * @returns {string} 模板渲染后内容
       */
      include: function (id, data) {
        var elem = document.getElementById(id);
        var template = elem ? elem.innerHTML : '';
        return template ? that.render(template, data) : '';
      },
    },
    vars,
  );

  // 编译模板
  that.compile(options.template);
};

/**
 * 渲染
 * @param {Object} template - 模板
 * @param {Object} data - 数据
 * @returns {string} 渲染后的模板内容
 */
Class.prototype.render = function (template, data) {
  var that = this;
  var options = that.config;

  // 获得模板渲染函数
  var compiler = template
    ? that.compile(template)
    : that.compilerCache || that.compile(options.template);

  // 获取渲染后的字符
  var html = (function () {
    data = data || options.data || {};
    try {
      return compiler(data);
    } catch (e) {
      template = template || options.template;
      return tools.error(
        e,
        {
          errorContext: that.extractErrorContext(template, data),
          template: template,
          type: 'Render',
        },
        options.error,
      );
    }
  })();

  // 缓存编译器
  if (options.cache && !template) {
    that.compilerCache = compiler;
  }

  return html; // 返回渲染后的字符
};

/**
 * 编译模板
 * @param {string} template - 原始模板
 * @returns {Function} 模板编译器，用于后续数据渲染
 */
Class.prototype.compile = function (template) {
  var that = this;
  var options = that.config;
  var openDelimiter = options.open;
  var closeDelimiter = options.close;
  var condense = options.condense;
  var regex = tools.regex;
  var placeholder = '\u2028'; // Unicode 行分隔符

  // console.log('compile');

  // 模板必须为 string 类型，且不能为空
  if (typeof template !== 'string' || !template) {
    return function () {
      return '';
    };
  }

  /**
   * 完整标签正则
   * @param {string[]} cores - 标签内部核心表达式，含：前置、主体、后置
   * @param {Object} sides - 标签两侧外部表达式
   * @returns {RegExp}
   */
  var tagRegex = function (cores, sides) {
    var arr = [
      '(?:' + openDelimiter + (cores[0] || '') + '\\s*)', // 界定符前置
      '(' + (cores[1] || '[\\s\\S]') + '*?)', // 标签主体
      '(?:\\s*' + (cores[2] || '') + closeDelimiter + ')', // 界定符后置
    ];
    sides = sides || {};
    sides.before && arr.unshift(sides.before); // 标签前面的表达式
    sides.after && arr.push(sides.after); // 标签后面的表达式
    return regex(arr.join(''));
  };

  // 匹配非输出类型标签两侧的换行符和空白符，避免渲染后占用一行
  var sidesRegex = condense ? ['', ''] : ['(?:(?:\\n)*\\s*)', '(?:\\s*?)'];
  var delimSides = {
    before: sidesRegex[0],
    after: sidesRegex[1],
  };

  /**
   * 清理多余符号
   * @param {string} body - 标签主体字符
   * @param {boolean} nowrap - 是否强制不换行
   * @returns {string} 清理后的字符
   */
  var clear = function (body, nowrap) {
    if (!condense) {
      // 还原语句中的 Unicode 行分隔符
      body = body.replace(regex(placeholder), nowrap ? '' : '\n');
    }
    body = body.replace(/\\(\\|")/g, '$1'); // 去除多余反斜杠
    return body;
  };

  // 纠正标签结构
  var correct = function (tpl) {
    return tpl.replace(
      regex('([}\\]])' + closeDelimiter),
      '$1 ' + closeDelimiter,
    );
  };

  // 模板解析
  var parse = (that.parse = function (tpl) {
    tpl = tpl || '';
    if (!tpl) return tpl;

    // 压缩连续空白符
    if (condense) {
      tpl = tpl.replace(/\t/g, ' ').replace(/\s+/g, ' ');
    }

    // 初始整理
    tpl = correct(tpl) // 纠正标签
      .replace(/(?=\\|")/g, '\\') // 转义反斜杠和双引号
      .replace(/\r?\n/g, condense ? '' : placeholder); // 整理换行符

    // 忽略标签 - 即区域中的内容不进行标签解析
    tpl = tpl.replace(
      tagRegex(['!', '', '!'], delimSides),
      function (str, body) {
        body = body.replace(
          regex(openDelimiter + '|' + closeDelimiter),
          function (tag) {
            return tag.replace(/(?=.)/g, '\\');
          },
        );
        return body;
      },
    );

    // 模板字符拼接
    var strConcatenation = function (body) {
      // 通过对 20k+ 行的模板进行编译测试， 发现 Chrome `+=` 性能竟优于 `push`
      // 1k 次循环 + 1k 行数据量进行模板编译+渲染，发现 `+=` 性能仍然优于 `push`
      // 考虑可能是 V8 做了 Ropes 结构优化？ 或跟模板采用「静态拼接」的实现有关（可能性更大）
      return ['";', body, '__laytpl__+="'].join('\n');
      // return ['");', body, '__laytpl__.push("'].join('\n');
    };

    // 解析输出标签
    var output = function (str, delimiter, body) {
      var _escape;

      if (!body) return '';
      body = clear(body, true);

      // 输出方式
      if (delimiter === '-') {
        // 原文输出，即不对 HTML 原文进行转义
        _escape = '';
      } else {
        // 转义输出
        _escape = '_escape';
      }

      return body
        ? strConcatenation(
            '__laytpl__+=' + _escape + '(' + body + ');',
            // '__laytpl__.push('+ _escape +'('+ body +'));'
          )
        : '';
    };

    // 解析 Scriptlet
    var statement = function (str, body) {
      if (!body) return '';
      body = clear(body);
      return strConcatenation(body);
    };

    // 标签风格
    if (options.tagStyle === 'modern') {
      // 2.11+ 版本风格
      // 注释标签 - 仅在模板中显示，不进行解析，也不在视图中输出
      tpl = tpl.replace(tagRegex(['#'], delimSides), '');
      // 输出标签
      tpl = tpl.replace(tagRegex(['(=|-)']), output);
      // Scriptlet 标签
      tpl = tpl.replace(tagRegex([], delimSides), statement);
    } else {
      // < 2.11 版本风格
      // Scriptlet 标签
      tpl = tpl.replace(tagRegex(['#'], delimSides), statement);
      // 输出标签
      tpl = tpl.replace(tagRegex(['(=|-)*']), output);
    }

    // 恢复换行符
    if (!condense) {
      tpl = tpl.replace(regex(placeholder), '\\n');
    }

    return tpl;
  });

  /**
   * 创建模板编译器
   * 请注意: 开发者在使用模板语法时，需确保模板中的 JS 语句不来自于页面用户输入。
   * 即模板中的 JS 语句必须在页面开发者自身的可控范围内，否则请避免使用该模板解析。
   */
  var createCompiler = (that.createCompiler = function (template, builder) {
    builder = builder || createBuilder(template);
    return new Function('laytpl', 'return ' + builder)(that.vars);
  });
  var createBuilder = (that.createBuilder = function (template, builder) {
    builder =
      builder ||
      [
        'function(d){',
        '"use strict";',
        'var __laytpl__="",' +
          (function () {
            // 内部变量
            // 内部方法
            var arr = [];
            for (var key in that.vars) {
              arr.push((key === 'escape' ? '_' : '') + key + '=laytpl.' + key);
            }
            return arr.join(',');
          })() +
          ';',
        '__laytpl__="' + parse(template) + '";',
        'return __laytpl__;',
        // '__laytpl__.push("'+ parse(template) +'");',
        // 'return __laytpl__.join("");',
        '};',
      ].join('\n');
    // console.log(builder);
    return builder;
  });

  try {
    return createCompiler(template); // 返回编译器
  } catch (e) {
    delete that.compilerCache;
    return function () {
      return tools.error(
        e,
        {
          errorContext: that.extractErrorContext(template),
          template: template,
          type: 'Compile',
        },
        options.error,
      );
    };
  }
};

/**
 * 获取模板出错行上下文
 * @param {string} template - 原始模板
 * @param {Object} data - 数据
 * @returns {string}
 */
Class.prototype.extractErrorContext = function (template, data) {
  var that = this;

  // 给模板每行开头添加行号标记
  var lineNum = 1; // 行号
  var templateArr = template.split(/\r?\n/g);

  template = template.replace(/(?=^)/gm, function () {
    return '/*LINE:' + lineNum++ + '*/';
  });

  var builder = that.createBuilder(template);
  var builderArr = builder.split(/\r?\n/);
  var sourceURL = 'laytpl.builder.map';

  // 模板出错行上下文
  var errorContext = function (errLineNum) {
    errLineNum = parseInt(errLineNum) - 1;

    var arr = [''];
    var contextLines = 3; // 错误行上下延伸的行数
    var start = Math.max(0, errLineNum - contextLines);
    var end = Math.min(templateArr.length, errLineNum + contextLines);

    for (; start <= end; start++) {
      arr.push(
        (start == errLineNum ? '? ' : '  ') +
          (start + 1 + '| ') +
          templateArr[start],
      );
    }

    return arr.join('\n') + '\n';
  };

  try {
    builder += '\n//# sourceURL=' + sourceURL; // 添加映射
    var compiler = that.createCompiler(template, builder);
    if (data) compiler(data);
  } catch (e) {
    // 提取堆栈报错行号
    var stackLineNumRegxp = tools.regex(
      sourceURL.replace(/\./g, '\\.') + ':(\\d+)',
      'i',
    );
    var stackLineNum = (e.stack.match(stackLineNumRegxp) || [])[1] || 0;

    // 提取模板实际行号
    var extractErrLineNum = function (stackLineNum, isRecursion) {
      var lineNumRegxp = isRecursion
        ? /\/\*LINE:(\d+)\*\/[^*]*$/
        : /\/\*LINE:(\d+)\*\//;
      var errLineNum =
        String(builderArr[stackLineNum - 1]).match(lineNumRegxp) || [];
      errLineNum = errLineNum[1];

      // 若当前行未找到行号映射，则递归查找上一行
      if (!errLineNum && stackLineNum > 0) {
        return extractErrLineNum(stackLineNum - 1, true);
      }

      return errLineNum;
    };

    // 此处减去 anonymous 开头占用的 2 行
    var errLineNum = extractErrLineNum(stackLineNum - 2);

    // 若未找到映射行号，则直接返回 SyntaxError 对象（通过 DevTools 映射源查看模板行号标记）
    return errLineNum ? errorContext(errLineNum) : e;
  }
};

/**
 * 创建实例
 * @param {string} template - 模板
 * @param {Object} options - 选项
 * @returns {Object}
 */
var laytpl = function (template, options) {
  var inst = new Class(template, options);
  return thisModule.call(inst);
};

/**
 * 扩展模板内部变量
 * @param {Object} variables - 扩展内部变量，变量值通常为函数
 */
laytpl.extendVars = function (variables) {
  Object.assign(vars, variables);
};

/**
 * 设置默认配置
 * @param {Object} options - 选项
 */
laytpl.config = laytpl.set = function (options) {
  Object.assign(config, options);
};

// 输出接口
export { laytpl };
